/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.variable;

import tal.drivechain.variable.types.IndexableType;
import tal.drivechain.variable.types.DataType;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.expression.dcExpression;
import tal.drivechain.wildcard.DoubleElement;
import tal.drivechain.wildcard.StringElement;
import tal.drivechain.wildcard.Wildcard;
import tal.drivechain.wildcard.WildcardElement;
import tal.drivechain.parsing.ParsingAid;
import tal.drivechain.parsing.Tokenizer;

/**
 * VarTableAction Represents an action on a VarTable object. Either get a variable from the table
 * or set a variable in the table. 
 * The action can then be applied on a VarTable.
 * 
 * parse an object from the following syntax:
 *  name[index1]..[indexN]
 * 
 * @author Main
 */
public class VarTableAction {

    public enum Action { SET_NEW, SET_ANY, SET_EXISTING, GET }
    String name;
    Action action;
    List modifiers = null;
    String lookup;

    public VarTableAction(String name, Action action) {
        this.action = action;
        this.name = name;
        if (action!=Action.GET && !VarTable.isValidVarName(name))
            throw new IllegalArgumentException("Illegal variable name: " + name);
    }

    public void addModifier(Object mod) { 
        if (mod instanceof String || mod instanceof Wildcard || mod instanceof dcExpression) {
            if (modifiers==null) modifiers = new ArrayList();
            modifiers.add(mod);
        }
    }

    private void setLookupString(String lookup) {
        this.lookup = lookup;
    }

    public static VarTableAction parseAction(Action action, String lookup) {
        if (lookup==null || lookup.length()==0) throw new IllegalArgumentException("Lookup string is null or zero-length.");
        Stack modifiers = new Stack();
        String name = parseModifiers(modifiers, lookup, action);
        
        VarTableAction a = new VarTableAction(name, action);
        a.setLookupString(lookup);
        while (!modifiers.empty()) 
            a.addModifier(modifiers.pop());
        
        return a;
    }

    public Action getAction() {
        return action;
    }

    private static String parseModifiers(Stack modifiers, String lookup, Action action) throws IllegalArgumentException {
        // adds last modifier to the stack and remove from returned string
        lookup = lookup.trim();
        
        //find last [
        int lastBracket = ParsingAid.lastIndexOfUnnested(lookup, '[', "[", "]");
        if (lastBracket==-1) { // no modifier
            return lookup;
        } else { // dealing with index modifier
            int endBracket = Tokenizer.balanced(lookup, lastBracket, '[', ']');
            String index = lookup.substring(lastBracket+1, endBracket);
            modifiers.push(parseIndex(index, action));
            return parseModifiers(modifiers, lookup.substring(0, lastBracket), action);
        }        
    }
    
    private static Object parseIndex(String index, Action action) {
        boolean isString = false;
        if (index.charAt(0)=='#') {
            index = index.substring(1, index.length());
            isString = true;
        }
        
        if (action==Action.GET) { // returns Wildcard
            WildcardElement element;
            if (isString) element = new StringElement();
            else element = new DoubleElement();
            return Wildcard.parseDefinition("[" + index + "]", element);
        } else { // returns Experssion {
            DataType d;
            if (isString) d = DataType.STRING;
            else d = DataType.INT;
            if (ParsingAid.isNumber(index)) index = "${" + index + "}";
            return new dcExpression(index, d);
        }
    }

    public Variable get(ChainNode context) {
        if (action!=Action.GET)
            throw new IllegalArgumentException("action mismatch: trying to get from vartable while action is " + action.name());
        
        Variable var = context.findVariable(name);

        if (var==null) return null;
        else {
            if (modifiers!=null && modifiers.size()>0) {
                for (Object modifier : modifiers) {
                    if (modifier instanceof Wildcard) { // index 
                        if (var.getDataType() instanceof IndexableType) {
                            IndexableType type = (IndexableType)var.getDataType();
                            Wildcard idx = (Wildcard)modifier;
                            var = type.getByIndex(idx, var, context);
                        } else throw new IllegalArgumentException("Trying to get index of non indexable variable: " + var.toString(context));
                    } else throw new IllegalArgumentException("Invalid modifier object class for GET vartable action: " + modifier.getClass().getName());
                }
            }
            return var;
        }
    }
    
    public Variable set(Variable var, VarTable applyOn, ChainNode context) {
        if (action==Action.GET)
            throw new IllegalArgumentException("action mismatch: trying to set from vartable while action is " + action.name());

        // TODO does copying the var on every set really the right choice??
        var = var.copyVar();

        // try to find existing var by name.
        Variable v = applyOn.get(name);
        
        if (v==null) {
            if (action==Action.SET_EXISTING)
                throw new IllegalArgumentException("Variable " + name + " does not exist. New entry is not allowed.");
        } else if (action==Action.SET_NEW) 
            throw new IllegalArgumentException("Variable " + name + " already exists. Overwriting is not allowed.");
        
        //check for indexes
        if (modifiers!=null && modifiers.size()>0) {
            if (v==null)
                throw new IllegalArgumentException("Trying to set value by index, but the variable was not found.");

            if (!(v.getDataType() instanceof IndexableType))
                throw new IllegalArgumentException("Trying to set value by index, but the variable data type doesn't support indexes: " + v);

            // if we got here then lookup supports indexes and was found in the table.
            
            Object workon = v.getInternalValue();
            for (Object modifier : modifiers) {
                if (modifier instanceof dcExpression) {
                    // should go over every index and set the last index.
                    IndexableType type = (IndexableType)v.getDataType();
                    if (modifiers.indexOf(modifier)==modifiers.size()-1) { //last
                        v.setValue(type.setByIndex(workon, var, (dcExpression)modifier, context), true);
                    } else {
                        if (type.isNestingAllowed()) {
                            workon = type.getNextDimension(workon, modifier, context);
                        } else throw new IllegalArgumentException("SET action with multiple indexes is only allowed for multi dimensional arrays.");
                    }
                } else throw new IllegalArgumentException("Invalid modifier object class for SET vartable action: " + modifier.getClass().getName());
            }
        } else v = var;
        v.setName(name);
        applyOn.put(name, v);
        applyOn.sendSetVarMessage(v);
        return v;
    }

    public String getName() {
        return name;
    }

    public String getLookupString() { return lookup; }
}
